﻿//************************************************************************************************************
// CoNatural.Data.dll
// Author: Roger Torres
//************************************************************************************************************
// You can use, modify, and distribute the source code and executable programs based on the source code.
// This source code is provided "as is" and without warranties as to performance or merchantability. 
// The author and/or distributors of this source code may have made statements about this source code. 
// Any such statements do not constitute warranties and shall not be relied on by the user in deciding 
// whether to use this source code. This source code is provided without any express or implied warranties 
// whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, 
// no warranty of fitness for a particular purpose is offered. The user is advised to test the source code 
// thoroughly before relying on it. The user must assume the entire risk of using the source code.
//************************************************************************************************************

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using CoNatural.Binders;

namespace CoNatural.Data.SqlClient {
	/// <summary>
	/// Connection supporting asynchronous operations.
	/// </summary>
	public class SqlClientAsyncConnection : ConnectionBase {
		/// <summary>
		/// StateObject holds references that can be used
		/// to cancel the executing sql command and handle exceptions.
		/// </summary>
		public class StateObject {
			public SqlClientAsyncCommandExecutionContext Context { get; private set; }
			internal int KeyColumnIndex { get; private set; }

			internal StateObject(SqlClientAsyncCommandExecutionContext context) {
				Context = context;
			}

			internal StateObject(SqlClientAsyncCommandExecutionContext context, int keyColumnIndex) {
				Context = context;
				KeyColumnIndex = keyColumnIndex;
			}
		}

		/// <summary>
		/// Generic state object for target readers
		/// </summary>
		/// <typeparam name="T">The type of the target.</typeparam>
		public class StateObject<T> : StateObject {
			internal T Target { get; private set; }

			internal StateObject(SqlClientAsyncCommandExecutionContext context, T target)
				: base(context) {
				Target = target;
			}
		}

		public class SqlClientAsyncCommandExecutionContext : CommandExecutionContext {
			internal SqlClientAsyncCommandExecutionContext(ConnectionBase connection, ICommand command) : base(connection, command) { }
			internal SqlClientAsyncCommandExecutionContext(ConnectionBase connection, ICommand command, object obj) : base(connection, command, obj) { }

			internal IAsyncResult BeginExecuteNonQuery(AsyncCallback callback) {
				return BeginExecuteNonQuery(callback, new StateObject(this));
			}

			internal IAsyncResult BeginExecuteNonQuery(AsyncCallback callback, StateObject state) {
				return ((SqlCommand)DbCommand).BeginExecuteNonQuery(callback, state);
			}

			internal int EndExecuteNonQuery(IAsyncResult asyncResult) {
				return ((SqlCommand)DbCommand).EndExecuteNonQuery(asyncResult);
			}

			internal IAsyncResult BeginExecuteReader(AsyncCallback callback) {
				return BeginExecuteReader(callback, new StateObject(this));
			}

			internal IAsyncResult BeginExecuteReader(AsyncCallback callback, StateObject state) {
				return ((SqlCommand)DbCommand).BeginExecuteReader(callback, state);
			}

			internal SqlDataReader EndExecuteReader(IAsyncResult asyncResult) {
				return ((SqlCommand)DbCommand).EndExecuteReader(asyncResult);
			}
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="connectionString">The connection string.</param>
		public SqlClientAsyncConnection(string connectionString) : base(connectionString, new Data.Binders.DataBinderBase(new SqlClientDbProvider(), null)) { }

		/// <summary>
		/// Executes command invoking ADO.NET Command.ExecuteNonQuery internally.
		/// Output parameters can be used to pass results back to command.
		/// </summary>
		/// <param name="iCommand">The command.</param>
		/// <returns>The number of records affected.</returns>
		public IAsyncResult BeginExecute(ICommand iCommand, AsyncCallback callback) {
			SqlClientAsyncCommandExecutionContext ctx = null;
			try {
				ctx = new SqlClientAsyncCommandExecutionContext(this, iCommand);
				return ctx.BeginExecuteNonQuery(callback);
			}
			catch (Exception ex) {
				ctx.HandleError(ex);
				ctx.Close();
			}
			return null;
		}

		public int EndExecute(IAsyncResult asyncResult) {
			StateObject state = (StateObject)asyncResult.AsyncState;
			try {
				return state.Context.EndExecuteNonQuery(asyncResult);
			}
			catch (Exception ex) {
				state.Context.HandleError(ex);
			}
			finally {
				state.Context.Close();
			}
			return 0;
		}

		/// <summary>
		/// Executes command as data reader, materializing all records into a list of instances of T.
		/// </summary>
		/// <typeparam name="T">The type we are materializing.</typeparam>
		/// <param name="iCommand">The command.</param>
		/// <param name="callback">The callback delegate that materializes data records into instances of T.</param>
		/// <returns>An enumerable list of T.</returns>
		public IAsyncResult BeginExecuteReader<T>(ICommand iCommand, AsyncCallback callback) where T : class, new() {
			SqlClientAsyncCommandExecutionContext ctx = null;
			try {
				ctx = new SqlClientAsyncCommandExecutionContext(this, iCommand);
				return ctx.BeginExecuteReader(callback);
			}
			catch (Exception ex) {
				ctx.HandleError(ex);
				ctx.Close();
			}
			return null;
		}

		public IEnumerable<T> EndExecuteReader<T>(IAsyncResult asyncResult) where T : class, new() {
			StateObject state = (StateObject)asyncResult.AsyncState;
			return EndExecuteReader<T>(asyncResult, null);
		}

		public IEnumerable<T> EndExecuteReader<T>(IAsyncResult asyncResult, ReaderCallback<T> callback) where T : class, new() {
			StateObject state = (StateObject)asyncResult.AsyncState;
			ReaderExecutionContext<T> context = state.Context as ReaderExecutionContext<T>;
			IList<T> result = new List<T>();
			try {
				using (IDataReader dataReader = state.Context.EndExecuteReader(asyncResult)) {
					context.ResultIndex = 1;
					do {
						while (dataReader.Read()) {
							result.Add(callback != null ? callback(context) : context.Materialize());
						}
						context.ResultIndex = context.ResultIndex + 1;
					}
					while (dataReader.NextResult());
					dataReader.Close();
				}
			}
			catch (Exception ex) {
				state.Context.HandleError(ex);
			}
			finally {
				state.Context.Close();
			}
			return result;
		}

		/// <summary>
		/// Executes command as data reader, materializing all records inside a target object. 
		/// </summary>
		/// <typeparam name="T">The target type.</typeparam>
		/// <param name="iCommand">The command.</param>
		/// <param name="target">The target object.</param>
		/// <param name="callback">The callback delegate that materializes data records inside the target object.</param>
		/// <returns>The number of materialized records.</returns>
		public IAsyncResult BeginExecuteTargetReader<T>(T target, ICommand iCommand, AsyncCallback callback) where T : class, new() {
			SqlClientAsyncCommandExecutionContext ctx = null;
			try {
				ctx = new SqlClientAsyncCommandExecutionContext(this, iCommand);
				return ctx.BeginExecuteReader(callback, new StateObject<T>(ctx, target));
			}
			catch (Exception ex) {
				ctx.HandleError(ex);
				ctx.Close();
			}
			return null;
		}

		public int EndExecuteTargetReader<T>(IAsyncResult asyncResult, ReaderCallback<T> callback) where T : class, new() {
			int count = 0;
			StateObject<T> state = (StateObject<T>)asyncResult.AsyncState;
			ReaderExecutionContext<T> context = state.Context as ReaderExecutionContext<T>;
			try {
				using (IDataReader dataReader = state.Context.EndExecuteReader(asyncResult)) {
					context.ResultIndex = 1;
					do {
						while (dataReader.Read()) {
							var t = callback != null ? callback(context) : context.Materialize();
							count++;
						}
						context.ResultIndex = context.ResultIndex + 1;
					}
					while (dataReader.NextResult());
					dataReader.Close();
				}
			}
			catch (Exception ex) {
				state.Context.HandleError(ex);
			}
			finally {
				state.Context.Close();
			}
			return count;
		}

		/// <summary>
		/// Executes command implementing ICommand(of T) interface with hooks to manipulate instance bound to command.
		/// </summary>
		/// <typeparam name="T">The type of the instance.</typeparam>
		/// <param name="iCommand">The command.</param>
		/// <param name="instance">The instance of T.</param>
		/// <param name="callback">The async callback.</param>
		/// <returns>The number of rows affected.</returns>
		public IAsyncResult BeginExecute<T>(ICommand iCommand, T instance, AsyncCallback callback) {
			SqlClientAsyncCommandExecutionContext ctx = null;
			try {
				ctx = new SqlClientAsyncCommandExecutionContext(this, iCommand, instance);
				return ctx.BeginExecuteNonQuery(callback, new StateObject<T>(ctx, instance));
			}
			catch (Exception ex) {
				ctx.HandleError(ex);
				ctx.Close();
			}
			return null;
		}

		public int EndExecute<T>(IAsyncResult asyncResult) {
			StateObject<T> state = (StateObject<T>)asyncResult.AsyncState;
			try {
				return state.Context.EndExecuteNonQuery(asyncResult);
			}
			catch (Exception ex) {
				state.Context.HandleError(ex);
			}
			finally {
				state.Context.Close();
			}
			return 0;
		}
	}
}
